package edu.cth.ai23;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Transparency;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.Random;

import javax.imageio.ImageIO;

class EightPuzzle implements IProblem {
    protected int[][] values = new int[3][3];
    protected int emptyTileX;
    protected int emptyTileY;
    String[] heuristicMethod = {"Manhattan Distance", "Manhattan Distance + Nilsson's Sequence Score"};
    int heuristicChoice = 0;
    int problemSearchDepth = 5;
    boolean greed = false;
    Image[] tiles = new BufferedImage[8];
    
    {
    	emptyTileX = 0;
    	emptyTileY = 0;
    	int value = 0;
    	for (int i = 0; i < 3; i++) {
    		for (int j = 0; j < 3; j++) {
    			values[j][i] = value;
    			value++;
    		}
    	}
    }
    
    Font font = new Font("Impact", Font.PLAIN, 32);
    Font smallerFont = new Font("Impact", Font.PLAIN, 18);
    
    public enum PuzzleMoves {

        up, down, right, left;
    }

    public static final PuzzleMoves[] PUZZLEVALUES = {
        PuzzleMoves.up, PuzzleMoves.down, PuzzleMoves.right, PuzzleMoves.left};
    
    public static final PuzzleMoves[] REVPUZZLEVALUES = {
        PuzzleMoves.down, PuzzleMoves.up, PuzzleMoves.left, PuzzleMoves.right};
    
    public EightPuzzle() {
    	for (int i=0; i<8; i++) { 
    	BufferedImage sourceImage = null;
			try {
				URL url = this.getClass().getClassLoader().getResource("images/" + (i+1)+ ".png");
				if (url == null) {
					System.err.println("Can't find ref: " +(i+1) + ".png");
				}	
				
				sourceImage = ImageIO.read(url);
		    	GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
				this.tiles[i] = gc.createCompatibleImage(sourceImage.getWidth(),sourceImage.getHeight(),Transparency.BITMASK);
				this.tiles[i].getGraphics().drawImage(sourceImage,0,0,null);
				
			} catch (IOException e) {
				System.err.println("Failed to load: ");
			}
    	}
    	reset();
    }
    
    public void copyValues(IProblem problem) {

        EightPuzzle ep2 = (EightPuzzle)(problem); //assume it is an eight puzzle
        this.heuristicChoice = ep2.heuristicChoice;
        this.problemSearchDepth = ep2.problemSearchDepth;
        emptyTileX = ep2.emptyTileX;
        emptyTileY = ep2.emptyTileY;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                values[i][j] = ep2.values[i][j];
            }
        }
    }

    public IProblem getProblemCopy() {
    	EightPuzzle ep2 = new EightPuzzle();
    	ep2.copyValues((IProblem) this);
    	return ep2;
    }

    public void scramble() {    	
		Random random = new Random();
		int randomMove;
		for(int i=0; i<100; i++) {
			Integer[] movesIndexes = getNoOfMoves(0);
			randomMove = random.nextInt(movesIndexes.length);
			this.move(movesIndexes[randomMove], this);
		}
    }

    public void draw(Graphics g) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                g.setColor(Color.white);
                g.draw3DRect(250+i*100, 150+j*100, 100, 100, true);
                if (this.values[i][j] != 0) {
                	g.drawImage(tiles[this.values[i][j]-1], 250+i*100+10, 150+j*100+10, null);
                }
            }
        }
        
        g.setColor(Color.gray);
        g.drawString("Using -> " + heuristicMethod[heuristicChoice] + " " + this.evaluate() + " Search Depth: " + problemSearchDepth + " Greedy: " + this.greed, 10, 10);
        g.setColor(Color.white);
        g.drawString("Using -> " + heuristicMethod[heuristicChoice] + " " + this.evaluate() + " Search Depth: " + problemSearchDepth + " Greedy: " + this.greed, 10+1, 10+1);

        g.setColor(Color.red);
        g.fill3DRect(250+emptyTileX*100+10, 150+emptyTileY*100+10, 100-20, 100-20, true);

    }

    public void reset() {
    	int value = 0;
    	emptyTileX = 0;
    	emptyTileY = 0;
    	for (int i = 0; i < 3; i++) {
    		for (int j = 0; j < 3; j++) {
    			values[j][i] = value;
    			value++;
    		}
    	}
    }
        
    public int getSearchDepth() {
    	return problemSearchDepth;
    }
    
    public boolean getGreed() {
    	return this.greed;
    }
    
    public int generateNilssonSequence() {
    	//Integer[] sequence = new Integer[9];
    	//int index = 0;
    	int nilssonScore=0;
    	/*for (int i=0; i<3; i++) {
    		for(int j=0; j<3; j++) {
    			sequence[index] = values[i][j];
    			index++;
    		}
    	}
    	for (int i=0; i<8; i++) {
    		if (sequence[i+1] != (sequence[i]+1)) {
    			nilssonScore += 2;
    		}
    	}*/
    	if (values[0][0] != 0){
    		nilssonScore++;
    	}
    	
    	for (int i=1; i<8; i++) {
    		if((values[i%3][i/3]+1) != values[(i+1)%3][(i+1)/3]) {
    			nilssonScore += 2;
    		}
    	}
    	
    	return nilssonScore;
    }
    
    public int evaluate() {
    	int evalSum = 0;
    	switch (heuristicChoice) {
    	case 0:
    		evalSum = this.manhattanDistance();
    		break;
    	case 1:
    		evalSum = this.manhattanDistance() + (3*this.generateNilssonSequence());
    		break;
    	}
    	return evalSum;
    }

    
    public int manhattanDistance() {
    	int number = 0;
    	int distance = 0;
    	for (int i=0; i<3; i++) {
    		for (int j=0; j<3; j++) {
    			number = this.values[i][j];
    			switch (number) {
    			case 0: //Row 1 calculations
    				//distance += Math.abs(i-0) + Math.abs(j-0);
    				break;
    			case 1:
    				distance += Math.abs(i-1) + Math.abs(j-0);
    				break;
    			case 2:
    				distance += Math.abs(i-2) + Math.abs(j-0);
    				break;
    			
    			case 3: //Row 2 calculations
    				distance += Math.abs(i-0) + Math.abs(j-1);
    				break;
    			case 4:
    				distance += Math.abs(i-1) + Math.abs(j-1);
    				break;
    			case 5:
    				distance += Math.abs(i-2) + Math.abs(j-1);
    				break;
    				
    			case 6: //Row 3 calculations
    				distance += Math.abs(i-0) + Math.abs(j-2);
    				break;
    			case 7:
    				distance += Math.abs(i-1) + Math.abs(j-2);
    				break;
    			case 8:
    				distance += Math.abs(i-2) + Math.abs(j-2);
    				break;
    			}
    		}
    	}    
    	return distance;
    }
    
    public int misplacement() {
        int valueRights = 0;

        if (values[1][0] != 1) {
        	valueRights++;
        }
        if (values[2][0] != 2) {
        	valueRights++;
        }
        if (values[0][1] != 3) {
        	valueRights++;
        }
        if (values[1][1] != 4) {
        	valueRights++;
        }
        if (values[2][1] != 5) {
        	valueRights++;
        }
        if (values[0][2] != 6) {
        	valueRights++;
        }
        if (values[1][2] != 7) {
        	valueRights++;
        }
        if (values[2][2] != 8) {
        	valueRights++;
        }
        return valueRights;
    }    

    public void handleKeyPress(int keyCode) {

        try {
            if (keyCode == KeyEvent.VK_UP) {
            	Integer[] movesIndexes = getNoOfMoves(0);
            	boolean legal = false;
            	for (int i=0; i<movesIndexes.length; i++) {
            		if (movesIndexes[i] == 0) {
            			legal = true;
            		}
            	}
            	if (legal) {
            		move(0, this);
            	}
            }

            if (keyCode == KeyEvent.VK_DOWN) {
            	Integer[] movesIndexes = getNoOfMoves(0);
            	boolean legal = false;
            	for (int i=0; i<movesIndexes.length; i++) {
            		if (movesIndexes[i] == 1) {
            			legal = true;
            		}
            	}
            	if (legal) {
            		move(1, this);
            	}
            }

            if (keyCode == KeyEvent.VK_RIGHT) {
            	Integer[] movesIndexes = getNoOfMoves(0);
            	boolean legal = false;
            	for (int i=0; i<movesIndexes.length; i++) {
            		if (movesIndexes[i] == 2) {
            			legal = true;
            		}
            	}
            	if (legal) {
            		move(2, this);
            	}
            }

            if (keyCode == KeyEvent.VK_LEFT) {
            	Integer[] movesIndexes = getNoOfMoves(0);
            	boolean legal = false;
            	for (int i=0; i<movesIndexes.length; i++) {
            		if (movesIndexes[i] == 3) {
            			legal = true;
            		}
            	}
            	if (legal) {
            		move(3, this);
            	}
            }
        if (keyCode == KeyEvent.VK_H){
        	heuristicChoice++;
        	if(heuristicChoice>1) {
        		heuristicChoice = 0;
        	}
        }
        if (keyCode == KeyEvent.VK_Q){
        	problemSearchDepth++;
        	if(problemSearchDepth>20) {
        		problemSearchDepth = 0;
        	}
        }
            if (keyCode == KeyEvent.VK_G) {
            	this.greed = (!this.greed);
            }

        } catch (NullPointerException e) {
            //kommer hända
        }
    }

    public Integer[] getNoOfMoves(int previousIndex) {
    	
    	PuzzleMoves[] moves = getMoves();
    	
    	Integer[] movesIndexes = new Integer[moves.length];
    	
    	for (int i=0; i<moves.length; i++) {
    		switch (moves[i]) {
    		case up:
    			movesIndexes[i] = 0;
    			break;
    		case down:
    			movesIndexes[i] = 1;
    			break;
    		case right:
    			movesIndexes[i] = 2;
    			break;
    		case left:
    			movesIndexes[i] = 3;
    			break;
    		}
    		
    	}
        return movesIndexes;
    }

    public void move(int moveIndex, IProblem problem) {
        
        PuzzleMoves move = PUZZLEVALUES[moveIndex];
        try {
            switch (move) {
            case up:
                    //System.out.println("up!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX][emptyTileY-1];
                    emptyTileY = emptyTileY - 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case down:
                    //System.out.println("down!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX][emptyTileY+1];
                    emptyTileY = emptyTileY + 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case right:
                    //System.out.println("right!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX+1][emptyTileY];
                    emptyTileX = emptyTileX + 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case left:
                    //System.out.println("left!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX-1][emptyTileY];
                    emptyTileX = emptyTileX - 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
    
public void reverseMove(int moveIndex, IProblem problem) {
        
        PuzzleMoves revMove = REVPUZZLEVALUES[moveIndex];
        try {
            switch (revMove) {
            case up:
                    //System.out.println("up!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX][emptyTileY-1];
                    emptyTileY = emptyTileY - 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case down:
                    //System.out.println("down!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX][emptyTileY+1];
                    emptyTileY = emptyTileY + 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case right:
                    //System.out.println("right!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX+1][this.emptyTileY];
                    emptyTileX = emptyTileX + 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            case left:
                    //System.out.println("left!");
                    values[emptyTileX][emptyTileY] = values[emptyTileX-1][emptyTileY];
                    emptyTileX = emptyTileX - 1;
                    values[emptyTileX][emptyTileY] = 0;
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private PuzzleMoves[] getMoves() {
    	PuzzleMoves moves[] = null;    	
    	switch (emptyTileX) {
        case 0:
        	switch (emptyTileY) {
        	case 0: /* valid moves: down, right */
        		moves = new PuzzleMoves[] {PUZZLEVALUES[1], PUZZLEVALUES[2]};
        		break;
        	case 1: /* valid moves: down, right, left */
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[1], PUZZLEVALUES[2]};
        		break;
        	case 2: /* valid moves: down, left */
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[2]};
        		break;
        	}
        	break;
        case 1:
        	switch (emptyTileY) {
        	case 0:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[1], PUZZLEVALUES[2], PUZZLEVALUES[3]};
        		break;
        	case 1:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[1], PUZZLEVALUES[2], PUZZLEVALUES[3]};
        		break;
        	case 2:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[2], PUZZLEVALUES[3]};
        		break;
        	}
        	break;
        case 2:
        	switch (emptyTileY) {
        	case 0:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[1], PUZZLEVALUES[3]};
        		break;
        	case 1:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[1], PUZZLEVALUES[3]};
        		break;
        	case 2:
        		moves = new PuzzleMoves[] {PUZZLEVALUES[0], PUZZLEVALUES[3]};
        		break;
        	}
        	break;
        }

        return moves;
    }

    public String getMoveName(int moveIndex) {
        return PUZZLEVALUES[moveIndex].toString();
    }

    public void moveSequence(Integer[] moveIndexes, IProblem problem) {

        for (int i = 0; i < moveIndexes.length; i++) {
            this.move(moveIndexes[i], problem);
            //EightPuzzleMove move = eightPuzzleMoves[moveIndex];
        }

    }
    
    public void reverseMoveSequence(Integer[] moveIndexes, IProblem problem) {
        for (int i = moveIndexes.length-1; i >= 0; i--) {
            this.reverseMove(moveIndexes[i], problem);
            //EightPuzzleMove move = eightPuzzleMoves[moveIndex];
        }
    }

    public int getSolvedEvaluation() {
        return 9;
    }
}